﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Workflow.Runtime.Hosting;
using System.Threading;
using System.Workflow.Runtime;
using System.Diagnostics;

namespace CommonWFLibrary
{
    /// <summary>
    /// This is the service that spawns thing off into a thread
    /// that does the actual work.
    /// </summary>
    public class LongRunningActivityBaseService : WorkflowRuntimeService
    {
        /// <summary>
        /// Callback to save our state
        /// </summary>
        /// <param name="obj">A pure .NET object that can be serialized using the standard methods</param>
        public delegate void SaveLRAState(object obj);

        /// <summary>
        /// Callback to fetch the state from disk
        /// </summary>
        /// <param name="obj">A pure .NET object that can be serialized using the standard methods</param>
        /// <returns></returns>
        public delegate object RestoreLRAState();

        /// <summary>
        /// Get this service up and going, and memorize which version of the WF runtime
        /// we are connected to so we can lookup queues and other things later.
        /// </summary>
        /// <param name="wr"></param>
        public LongRunningActivityBaseService()
        {
        }

        /// <summary>
        /// Init the service, provide callbacks to save and restore state if persistance is
        /// desired.
        /// </summary>
        /// <param name="sav"></param>
        /// <param name="res"></param>
        public LongRunningActivityBaseService(SaveLRAState sav, RestoreLRAState res)
        {
            _saver = sav;
            _restorer = res;
        }

        /// <summary>
        /// Callback to save our state - if persistance is being implemented, that is!
        /// </summary>
        private SaveLRAState _saver;

        /// <summary>
        /// Callback to pull our state from disk -- if persistance is being implemented, of course!
        /// </summary>
        private RestoreLRAState _restorer;

        /// <summary>
        /// Holds the cache of items we are keeping track of.
        /// </summary>
        private Dictionary<Guid, LongRunningActivityInfo> _cache = new Dictionary<Guid, LongRunningActivityInfo>();

        /// <summary>
        /// Add an item to the cache.
        /// </summary>
        /// <param name="st"></param>
        private void UpdatePersistanceCache(LongRunningActivityInfo st)
        {
            lock (_cache)
            {
                try
                {
                    _cache[st.instanceId] = st;
                    WriteOutPersistanceCache();
                }
                catch (Exception e)
                {
                    throw new LongRunningException("Error seralizing the long running persistance data", e);
                }
            }
        }

        /// <summary>
        /// Write out the persistance cache. The calling routine _must_ have already
        /// locked the _cache to make sure that we aren't stepping on others in the filesytsem!!!
        /// </summary>
        private void WriteOutPersistanceCache()
        {
            if (_saver != null)
            {
                _saver(_cache.Count == 0 ? null : _cache);
                Trace.WriteLineIf(_cache.Count != 0, "LongRunningService cache was updated");
            }
        }

        /// <summary>
        /// Done with a certian item. Clear the thing out!
        /// </summary>
        /// <param name="info"></param>
        private void UpdatePersistanceCacheClear(LongRunningActivityInfo info)
        {
            lock (_cache)
            {
                if (_cache.ContainsKey(info.instanceId))
                {
                    _cache.Remove(info.instanceId);
                    WriteOutPersistanceCache();
                }
            }
        }

        /// <summary>
        /// Updates the context in the cache. This is a "little" tricky because of race-conditions. For example,
        /// it could be the activity finished before we got notified of an updated context. So we don't want
        /// to keep the context around!
        /// </summary>
        /// <param name="info"></param>
        /// <param name="newcontext"></param>
        private void UpdatePersistanceCacheContext(LongRunningActivityInfo info, object newcontext)
        {
            lock (_cache)
            {
                if (_cache.ContainsKey(info.instanceId))
                {
                    _cache[info.instanceId]._context = newcontext;
                    WriteOutPersistanceCache();
                }
            }
        }

        /// <summary>
        /// After we get started, check to see if there are any workflows that
        /// are waiting... "kill" them... :-)
        /// </summary>
        protected override void OnStarted()
        {
            base.OnStarted();

            ///
            /// Watch for aborted WF's
            /// 

            Runtime.WorkflowAborted += new EventHandler<WorkflowEventArgs>(Runtime_WorkflowAborted);
            Runtime.WorkflowTerminated += new EventHandler<WorkflowTerminatedEventArgs>(Runtime_WorkflowTerminated);

            ///
            /// If there is a chance of finding some cached information, then pull it in and
            /// treat the WF accordingly.
            /// 

            if (_restorer != null)
            {
                Dictionary<Guid, LongRunningActivityInfo> info = _restorer() as Dictionary<Guid, LongRunningActivityInfo>;
                if (info != null)
                {
                    /// this isn't good!!! All these guys have crashed! So, we have to send them
                    /// an exception. If we are allowed to retry, reset the number and allow it
                    /// to go again.

                    foreach (var item in info.Keys)
                    {
                        if (info[item]._timesToRetry <= 0)
                        {
                            SendExceptionBack(info[item], new LongRunningException("Activity was waiting for response when runtime or workflow host crashed or the number of retries was exceeded for this long running activity."));
                        }
                        else
                        {
                            info[item]._timesToRetry = info[item]._timesToRetry - 1;
                            Trace.WriteLine("WF " + item.ToString() + " - restarting. Number times left: " + info[item]._timesToRetry);
                            StartAnActivity(info[item]);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Workflow is being terminated - kill it all off!
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Runtime_WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e)
        {
            LongRunningActivityInfo info = null;
            if (_cache.TryGetValue(e.WorkflowInstance.InstanceId, out info))
            {
                UpdatePersistanceCacheClear(info);
            }
        }

        /// <summary>
        /// A workflow has aborted. If we are watching it, then we need to remove it from our cache!
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void Runtime_WorkflowAborted(object sender, WorkflowEventArgs e)
        {
            LongRunningActivityInfo info = null;
            if (_cache.TryGetValue(e.WorkflowInstance.InstanceId, out info))
            {
                UpdatePersistanceCacheClear(info);
            }
        }

        /// <summary>
        /// Start a thread in the long-running activity to do the
        /// actual work.
        /// </summary>
        /// <param name="qName"></param>
        internal void StartAnActivity(string qName, MethodArgumentInterface callback, object arg, int retrytimes)
        {
            LongRunningActivityInfo info = new LongRunningActivityInfo(WorkflowEnvironment.WorkflowInstanceId, qName, callback, arg, retrytimes);
            StartAnActivity(info);
        }

        /// <summary>
        /// Start a thread to actually do the long-running activity work.
        /// </summary>
        /// <param name="info"></param>
        private void StartAnActivity(LongRunningActivityInfo info)
        {
            UpdatePersistanceCache(info);
            ThreadPool.QueueUserWorkItem(o => InternalRun(o as LongRunningActivityInfo), info);
        }

        /// <summary>
        /// Coordinate the long-running on a background thread. If the runtime dies while we are off doing something,
        /// then we assume that the results are lost. :(
        /// </summary>
        /// <param name="o"></param>
        /// <returns></returns>
        private void InternalRun(LongRunningActivityInfo info)
        {
            if (info._callback == null)
            {
                throw new LongRunningException("Callback is null in the service run method!");
            }

            LongRunningContext c = new LongRunningContext(this, info, Runtime);
            try
            {
                object newcontext = info._callback.DoWork(c, info._arg, info._context);
                if (State == WorkflowRuntimeServiceState.Started)
                {
                    UpdatePersistanceCacheContext(info, newcontext);
                }
                else
                {
                    Trace.WriteLine("Result ignored from long running activity: the WF Runtime had already been shutdown.");
                }
            }
            catch (Exception e)
            {
                if (State == WorkflowRuntimeServiceState.Started)
                {
                    c.Complete(e);
                }
            }
        }

        /// <summary>
        /// The work is done for this task. Record it and any result, and put it on the
        /// queue back. If the runtime is up and going, that is!
        /// </summary>
        /// <param name="info"></param>
        /// <param name="result"></param>
        internal void Complete(LongRunningActivityInfo info, object result)
        {
            ///
            /// Work is done. Get the queue and send the message back to
            /// the runtime! There is one thing we have to watch out for: the
            /// host has shutdown the runtime. If that is the case, then
            /// we need bug out and the result is lost.
            /// 
            /// There is a race condition below. The runtime could be shutting down
            /// as we are doing this -- and it could shutdown between the test at the top
            /// and the enqueue item below. Asked a question about this on MSDN:
            /// http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=3024808&SiteID=1
            /// 
            /// In short -- there are race conditions and there is nothing you can do except
            /// try to deal with it.
            /// 

            if (this.State == WorkflowRuntimeServiceState.Started)
            {
                try
                {
                    WorkflowInstance wi = Runtime.GetWorkflow(info.instanceId);
                    wi.EnqueueItem(info.resultQueueName, result, null, null);
                    UpdatePersistanceCacheClear(info);
                }
                catch (InvalidOperationException)
                {
                }
            }
        }

        /// <summary>
        /// Send an exception back to the workflow instance.
        /// </summary>
        /// <param name="lRAInfo"></param>
        /// <param name="longRunningException"></param>
        private void SendExceptionBack(LongRunningActivityInfo info, Exception e)
        {
            if (State == WorkflowRuntimeServiceState.Started)
            {
                WorkflowInstance wi = Runtime.GetWorkflow(info.instanceId);
                wi.EnqueueItem(info.resultQueueName, e, null, null);
            }
        }
    }
}
